package com.wecan.components.pay.service;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.wecan.api.saasdata.BusinessExceptionData;
import com.wecan.base.PO.BusinessException;
import com.wecan.base.PO.Order;
import com.wecan.base.VO.PayVO;
import com.wecan.base.constants.RedisKey;
import com.wecan.base.enums.ExceptionType;
import com.wecan.base.exception.AppException;
import com.wecan.cache.utils.RedisUtil;
import com.wecan.components.pay.constant.PayConstant;
import com.wecan.components.pay.properties.WxPayProperties;
import com.wecan.components.pay.utils.WXPayUtil;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.*;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.*;

import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;

/**
 * @Author liuzhihao
 * @Date 2022.02.22
 * @Version ${version}
 * @Description ${description}
 */
@Slf4j
public class PayService {

    @Autowired
    private WxPayProperties wxPayProperties;

    private PrivateKey privateKey;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private BusinessExceptionData businessExceptionData;

    @PostConstruct
    private void init() throws Exception {
        FileInputStream certKeyStream = new FileInputStream(new File(wxPayProperties.getCertKeyPath()));
        privateKey = PemUtil.loadPrivateKey(certKeyStream);
    }

    private Verifier getVerifier(String serialNum) throws Exception {
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(wxPayProperties.getMchId(), new WechatPay2Credentials(wxPayProperties.getMchId(), new PrivateKeySigner(serialNum, privateKey)), wxPayProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        return certificatesManager.getVerifier(wxPayProperties.getMchId());
    }

    /**
     * 构建支付客户端
     *
     * @return
     */
    public CloseableHttpClient createWeChatClient() throws Exception {
        String mchId = wxPayProperties.getMchId();
        String serialNumber = wxPayProperties.getSerialNumber();
        Verifier verifier = getVerifier(serialNumber);
        try {
            WechatPayHttpClientBuilder wechatPayHttpClientBuilder = WechatPayHttpClientBuilder.create().withMerchant(mchId, serialNumber, privateKey).withValidator(new WechatPay2Validator(verifier));
            return wechatPayHttpClientBuilder.build();
        } catch (Exception e) {
            throw new AppException("支付失败");
        }
    }

    /**
     * 构建消息
     *
     * @param appId
     * @param timeStamp
     * @param nonceStr
     * @param prepayId
     * @return
     */
    private String buildMessage(String appId, String timeStamp, String nonceStr, String prepayId) {
        return appId + "\n"
                + timeStamp + "\n"
                + nonceStr + "\n"
                + prepayId + "\n";
    }

    /**
     * 进行签名
     *
     * @param message
     * @return
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     * @throws IOException
     * @throws InvalidKeyException
     */
    private String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        Signature sha256withRSA = Signature.getInstance("SHA256withRSA");
        FileInputStream certKeyStream = new FileInputStream(new File(wxPayProperties.getCertKeyPath()));
        sha256withRSA.initSign(PemUtil.loadPrivateKey(certKeyStream));
        sha256withRSA.update(message);
        return Base64.getEncoder().encodeToString(sha256withRSA.sign());
    }

    /**
     * 验证签名
     *
     * @return
     */
    public JSONObject verify(HttpServletRequest request) throws Exception {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        String body = IOUtils.toString(bufferedReader);
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        String serialNum = request.getHeader(WECHAT_PAY_SERIAL);
        Verifier verifier = getVerifier(wxPayProperties.getSerialNumber());
        NotificationRequest build = new NotificationRequest.Builder().withSerialNumber(serialNum).withNonce(nonce).withTimestamp(timestamp).withSignature(signature).withBody(body).build();
        NotificationHandler notificationHandler = new NotificationHandler(verifier, wxPayProperties.getApiV3Key().getBytes());
        Notification notification = notificationHandler.parse(build);
        String decryptData = notification.getDecryptData();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("data", JSONObject.parseObject(decryptData));
        jsonObject.put("eventType", notification.getEventType());
        return jsonObject;
    }

    /**
     * 创建预付订单
     *
     * @return
     */
    public PayVO createOrder(Order order) {
        String appId = wxPayProperties.getAppId();
        String mchId = wxPayProperties.getMchId();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode objectNode = objectMapper.createObjectNode();
        int fee = order.getFinalFee().multiply(new BigDecimal(100)).intValue();
        objectNode.put("appid", appId)
                .put("description", order.getItemName())
                .put("mchid", mchId)
                .put("notify_url", PayConstant.WX_NOTIFY_URL)
                .put("out_trade_no", order.getId().toString())
                .putObject("amount").put("total", fee);
        try {
            objectMapper.writeValue(bos, objectNode);
            CloseableHttpClient httpClient = createWeChatClient();
            HttpPost httpPost = new HttpPost(PayConstant.WX_UNIFIED_ORDER_APP);
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Content-type", "application/json; charset=utf-8");
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject result = JSONObject.parseObject(bodyAsString);
            if (result.getString("code") != null) {
                return null;
            }
            String prepayId = result.getString("prepay_id");
            String redisKey = RedisKey.WX_PAY_ORDER + order.getId();
            // 两小时后过期，由于中间的传递等原因提前十秒结束准订单
            redisUtil.set(redisKey, prepayId, 7190);
            PayVO payVO = buildPayVO(prepayId);
            return payVO;
        } catch (Exception e) {
            throw new AppException("支付失败");
        }
    }

    /**
     * 构建支付结果
     *
     * @param prepayId
     * @return
     */
    public PayVO buildPayVO(String prepayId) throws Exception {
        PayVO payVO = new PayVO();
        payVO.setPrepayId(prepayId);
        payVO.setAppId(wxPayProperties.getAppId());
        payVO.setPackageStr("Sign=WXPay");
        payVO.setNonceStr(WXPayUtil.generateNonceStr());
        payVO.setPartnerId(wxPayProperties.getMchId());
        GregorianCalendar gc = new GregorianCalendar();
        int timestamp = (int) gc.getTimeInMillis();
        payVO.setTimestamp(String.valueOf(timestamp));
        String message = buildMessage(payVO.getAppId(), payVO.getTimestamp(), payVO.getNonceStr(), payVO.getPrepayId());
        String signStr = sign(message.getBytes("utf-8"));
        payVO.setSign(signStr);
        return payVO;
    }

    /**
     * 关闭订单
     *
     * @return
     */
    public boolean closeOrder(Order order) {
        String mchId = wxPayProperties.getMchId();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode objectNode = objectMapper.createObjectNode();
        objectNode.put("mchid", mchId);
        try {
            objectMapper.writeValue(bos, objectNode);
            CloseableHttpClient httpClient = createWeChatClient();
            HttpPost httpPost = new HttpPost(String.format(PayConstant.WX_CLOSE_ORDER_APP, order.getId()));
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Content-type", "application/json; charset=utf-8");
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);
            int code = response.getStatusLine().getStatusCode();
            if (code == 204) {
                // 订单关闭后将支付删除
                String redisKey = RedisKey.WX_PAY_ORDER + order.getId();
                redisUtil.del(redisKey);
                return true;
            } else if (code == 202) {
                throw new AppException("用户支付中，需要输入密码");
            } else if (code == 403) {
                throw new AppException("交易错误");
            } else if (code == 500) {
                throw new AppException("交易系统错误");
            } else if (code == 404) {
                throw new AppException("订单不存在");
            } else if (code == 400) {
                throw new AppException("订单已关闭");
            } else if (code == 403) {
                throw new AppException("余额不足");
            } else if (code == 429) {
                throw new AppException("请稍后再试");
            }
            return false;
        } catch (Exception e) {
            throw new AppException("关闭订单失败");
        }
    }

    /**
     * 退款
     *
     * @param order
     * @param reason
     * @return
     */
    public boolean refund(Order order, String reason) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode objectNode = objectMapper.createObjectNode();
        int fee = order.getFinalFee().multiply(new BigDecimal(100)).intValue();
        int refundFee = fee / 2;
        objectNode.put("out_trade_no", order.getId())
                .put("out_refund_no", order.getId())
                .put("reason", reason)
                .put("notify_url", PayConstant.WX_NOTIFY_URL)
                .putObject("amount").put("refund", refundFee).put("total", fee);
        try {
            objectMapper.writeValue(bos, objectNode);
            CloseableHttpClient httpClient = createWeChatClient();
            HttpPost httpPost = new HttpPost(String.format(PayConstant.WX_CLOSE_ORDER_APP, order.getId()));
            httpPost.addHeader("Accept", "application/json");
            httpPost.addHeader("Content-type", "application/json; charset=utf-8");
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String bodyAsString = EntityUtils.toString(response.getEntity());
            JSONObject result = JSONObject.parseObject(bodyAsString);
            if (result.getString("code") != null) {
                return false;
            }
            return true;
        } catch (Exception e) {
            BusinessException businessException = new BusinessException();
            businessException.setPrimaryId(order.getId());
            businessException.setType(ExceptionType.REFUND);
            businessExceptionData.add(businessException);
            return false;
        }
    }
}
